"use client"; import { use, useMemo, useState } from "react"; import Link from "next/link"; import clsx from "clsx"; import { ExternalLink } from "lucide-react"; import { useApi } from "@/lib/api"; import { ago, eventColor, fmtDate, fmtDuration, fmtTime, truncate } from "@/lib/format"; import { Bar, Empty, Metric, Panel, Skeleton, StatusPill, Tag, Table } from "@/components/ui"; import { GainChart } from "@/components/charts"; import { LiveFeed } from "@/components/LiveFeed"; import { WorldGraph, type GraphEdge, type GraphNode } from "@/components/WorldGraph"; import { EntityTable, type EntityRow } from "@/components/EntityTable"; interface Session { session_id: string; platform: string; account_alias: string; mode: string | null; goal: string | null; started_at: string; ended_at: string | null; health: string; job: { budget?: Record; query?: string; media_level?: number } | null; summary: { steps: number; entities: number; videos: number; network_responses: number; schemas: number; patterns_learned: number; ended_because: string; world: { nodes: number; edges: number; visited: number; multi_surface: number; by_type: Record }; connector: { confidence: number; page_types: number; network_schemas: number } } | null; event_counts: { event_type: string; n: number }[]; } interface Action { step: number; action_type: string; label: string; target_url: string | null; planner: string; expected_gain: number; novelty: number; relevance: number; reason: string; scores: { action_id: string; information_gain: number; penalties: string[] }[]; before_state: { url: string; page_type: string; visible_entities: number } | null; after_state: { url: string; page_type: string; visible_entities: number; new_entities: number; network_responses: number; dom_nodes_added: number } | null; success: boolean; error: string | null; duration_ms: number; ts: string; } interface PageRow { step: number; ts: string; payload: { url: string; title: string; page_type: string; confidence: number; signals: string[]; entities: number; dom_entities: number; network_entities: number; both_surfaces: number; field_agreements: number; field_conflicts: { field: string; network: unknown; dom: unknown }[]; videos: number; summary: string }; } interface Media { fingerprint: string; media_type: string; platform_media_id: string | null; title: string | null; author: string | null; duration_s: number | null; width: number | null; height: number | null; thumbnail_url: string | null; page_url: string | null; delivery: { kind: string; hostnames: string[]; manifest_url?: string } | null; frames: string[] | null; } interface Schema { shape_hash: string; fingerprint: { hostname: string; path_pattern: string; method: string; graphql_operation?: string }; schema: { candidate_entity_types: { type: string; confidence: number; path: string }[]; fields: { path: string; semantic?: { kind: string; confidence: number }; example?: string }[]; object_count: number }; observed_count: number; sample_url: string; } const TABS = ["decisions", "pages", "entities", "runtime apis", "media", "world model", "events"] as const; export default function SessionConsole({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); const [tab, setTab] = useState<(typeof TABS)[number]>("decisions"); const s = useApi(`/sessions/${id}`, { refreshMs: 10_000 }); const live = !s.data?.ended_at; const actions = useApi(`/sessions/${id}/actions`, { refreshMs: live ? 5000 : undefined }); const pages = useApi(`/sessions/${id}/pages`, { refreshMs: live ? 5000 : undefined }); const entities = useApi(tab === "entities" ? `/sessions/${id}/entities?limit=500` : null, { refreshMs: live ? 8000 : undefined }); const media = useApi(tab === "media" ? `/sessions/${id}/media` : null); const schemas = useApi(tab === "runtime apis" ? `/sessions/${id}/schemas` : null); const world = useApi<{ nodes: GraphNode[]; edges: GraphEdge[] }>(tab === "world model" ? `/sessions/${id}/world` : null, { refreshMs: live ? 10_000 : undefined }); const [selStep, setSelStep] = useState(null); const gainSeries = useMemo(() => (actions.data ?? []).map((a) => ({ step: a.step, expected_gain: Number(a.expected_gain), novelty: Number(a.novelty), relevance: Number(a.relevance), action_type: a.action_type, new_entities: a.after_state?.new_entities })), [actions.data]); const sess = s.data; const sum = sess?.summary; const lastPage = pages.data?.[pages.data.length - 1]; const selPage = selStep !== null ? pages.data?.find((p) => p.step === selStep) : lastPage; return (
sessions / {id}

{sess?.goal ?? "…"}

{sess && } {sess && {sess.platform}} {sess?.mode && {sess.mode}} {sess?.job?.query && “{sess.job.query}”} {sess && started {fmtDate(sess.started_at)} · {sess.ended_at ? `ended ${ago(sess.ended_at)}` : "running"}} {sum && · ended because {sum.ended_because}}
{[ ["steps", actions.data?.length ?? sum?.steps], ["entities", sum?.entities ?? sess?.event_counts.filter((c) => /DISCOVERED/.test(c.event_type)).reduce((a, c) => a + c.n, 0)], ["videos", sum?.videos], ["responses", sum?.network_responses ?? sess?.event_counts.find((c) => c.event_type === "NETWORK_RESPONSE_OBSERVED")?.n], ["schemas", sum?.schemas ?? sess?.event_counts.find((c) => c.event_type === "NETWORK_SCHEMA_DISCOVERED")?.n], ["learned", sum?.patterns_learned ?? sess?.event_counts.find((c) => c.event_type === "CONNECTOR_PATTERN_LEARNED")?.n], ].map(([k, v]) => (
{v ?? "—"}
{k as string}
))}
{TABS.map((t) => ( ))}
{tab === "decisions" && (
{gainSeries.length ? : no decisions yet} {actions.loading ? (
) : (
    {(actions.data ?? []) .slice() .reverse() .map((a) => (
  • setSelStep(a.step)} className={clsx("px-4 py-2.5 cursor-pointer hover:bg-white/[0.03]", selStep === a.step && "bg-cyan/5")}>
    #{a.step} {a.action_type.replace(/_/g, " ").toLowerCase()} {truncate(a.label.replace(/^Open \w+ E\d+: /, ""), 80)} gain {Number(a.expected_gain).toFixed(3)}
    nov {Number(a.novelty).toFixed(2)} rel {Number(a.relevance).toFixed(2)} {a.planner} {a.after_state && ( → {a.after_state.page_type} · +{a.after_state.new_entities} ent · {a.after_state.network_responses} resp · {a.duration_ms} ms )} {a.error && {truncate(a.error, 60)}}
    {a.reason}
  • ))}
)}
{selPage ? (
{selPage.payload.page_type} conf {Number(selPage.payload.confidence).toFixed(2)}
{selPage.payload.url}
{[ ["entities", selPage.payload.entities], ["dom", selPage.payload.dom_entities], ["network", selPage.payload.network_entities], ["both", selPage.payload.both_surfaces], ].map(([k, v]) => (
{v as number}
{k as string}
))}
{selPage.payload.field_conflicts?.length > 0 && (
{selPage.payload.field_conflicts.length} field conflict(s): {selPage.payload.field_conflicts.map((c) => c.field).join(", ")}
)}
{selPage.payload.summary}
) : ( no page state yet )}
{sess?.job?.budget ? (
steps used
) : ( )}
)} {tab === "pages" && (
{pages.data?.length ? ( {pages.data.map((p) => ( setSelStep(p.step)} className="cursor-pointer"> ))}
#{p.step} {fmtTime(p.ts)} {p.payload.page_type} {Number(p.payload.confidence).toFixed(2)} {truncate(p.payload.url, 80)} {p.payload.entities} ({p.payload.dom_entities} / {p.payload.network_entities} / {p.payload.both_surfaces}) {p.payload.field_agreements} {p.payload.field_conflicts?.length ? / {p.payload.field_conflicts.length}✗ : ""} {p.payload.videos}
) : ( no pages yet )}
)} {tab === "entities" && (
{entities.loading ? : entities.data?.length ? : no entities yet}
)} {tab === "runtime apis" && (
{schemas.loading ? ( ) : ( (schemas.data ?? []).map((sc) => (
{sc.fingerprint.method} {sc.fingerprint.hostname} {sc.fingerprint.path_pattern} {sc.fingerprint.graphql_operation && {sc.fingerprint.graphql_operation}} {sc.schema.candidate_entity_types.slice(0, 4).map((c) => ( {c.type} {Number(c.confidence).toFixed(2)} ))} seen {sc.observed_count}× · {sc.schema.object_count} objects · #{sc.shape_hash.slice(0, 8)}
sample: {sc.sample_url}
{sc.schema.fields.slice(0, 60).map((f) => ( ))}
{f.path} {f.semantic ? {f.semantic.kind} : "—"} {f.semantic ? Number(f.semantic.confidence).toFixed(2) : ""} {f.example}
)) )}
)} {tab === "media" && (
{media.loading ? ( ) : media.data?.length ? ( media.data.map((m) => (
{m.frames?.length ? (
{m.frames.map((f) => ( // eslint-disable-next-line @next/next/no-img-element ))}
) : m.thumbnail_url ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
no frame
)}
{m.title ?? m.platform_media_id}
{m.author ?? ""} {fmtDuration(m.duration_s)} {m.width ? {m.width}×{m.height} : null} {m.delivery?.kind && {m.delivery.kind}} {m.delivery?.hostnames?.length ? {m.delivery.hostnames.slice(0, 2).join(", ")} : null}
evidence →
)) ) : ( no media yet )}
)} {tab === "world model" && (
{world.data ? : }
)} {tab === "events" && (
    {(sess?.event_counts ?? []) .slice() .sort((a, b) => b.n - a.n) .map((c) => (
  • {c.event_type.toLowerCase()} {c.n}
  • ))}
{live ? : }
)}
); } function EventsTail({ id }: { id: string }) { const { data } = useApi<{ event_id: string; event_type: string; step: number; ts: string; payload: Record }[]>(`/sessions/${id}/events?limit=400`); if (!data) return ; return (
    {data.map((e) => (
  • {fmtTime(e.ts)} #{e.step ?? 0} {e.event_type.replace(/_/g, " ").toLowerCase()} {truncate(JSON.stringify(e.payload), 160)}
  • ))}
); }